# 引入fastapi暴露接口供前端调用
from fastapi import FastAPI, HTTPException, File, UploadFile
from pydantic import BaseModel
from datetime import datetime

import time
from pathlib import Path

import cv2
import torch
if torch.__version__ >= '1.8':
    import torch_npu
import torch.backends.cudnn as cudnn
from numpy import random
import base64

from models.experimental import attempt_load
from utils.datasets import LoadStreams, LoadImages
from utils.general import check_img_size, check_imshow, non_max_suppression, apply_classifier, \
    scale_coords, xyxy2xywh, set_logging
from utils.plots import plot_one_box
from utils.torch_utils import select_device, load_classifier, time_synchronized

# 定义 FastAPI 应用
app = FastAPI()

# 配置全局模型参数
weights = 'weights/last.pt'
imgsz = 640
device = select_device('')
nosave = False
augment = False
conf_thres = 0.2
iou_thres = 0.2
classes = None
agnostic_nms = False
save_conf = True
save_txt = True
view_img = False
save_img = True

# 初始化日志
set_logging()
half = device.type != 'cpu'  # 只有CUDA支持半精度
save_dir = Path('tmp/results') # 保存结果的目录

# 加载模型
model = attempt_load(weights, map_location=device)  # 加载模型
stride = int(model.stride.max())  # 获取步幅
imgsz = check_img_size(imgsz, s=stride)  # 检查图像大小

if half:
    model.half()  # 使用半精度

torch.npu.set_compile_mode(jit_compile=False)  # 设置编译模式

def detect(image_url: str):
    t0 = time.time()
    global model
    source = image_url
    
    # 是否为网络路径
    webcam = source.isnumeric() or source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))

    if webcam:
        cudnn.benchmark = True
        dataset = LoadStreams(source, img_size=imgsz, stride=stride)
    else:
        dataset = LoadImages(source, img_size=imgsz, stride=stride)

    print('dataset',dataset)

    names = model.module.names if hasattr(model, 'module') else model.names
    colors = [[random.randint(0, 255) for _ in range(3)] for _ in names]

    # Run inference确保模型在实际输入数据之前至少运行一次
    # 创建一个形状为(1, 3, imgsz, imgsz)的全零张量，其中imgsz是输入图像的尺寸。这个张量模拟了一个批量大小为1的单张图像的输入。
    if device.type != 'cpu':
        model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters())))  # run once
    old_img_w = old_img_h = imgsz
    old_img_b = 1

    # 方法最终返回的数据
    result_image = None # 框选后的图片
    result_text = [] # 框选后的检测类别及其置信度
    result_info = {} # 检测结果的数据，包括width、length、size、result
    red_bump_size = None # 红肿块平均直径大小, mm
    bottle_diameter = 32  # mm, 农夫山泉瓶盖的直径
    result = None # 检测的最终结果：阴性/一般阳性/中度阳性/强阳性
    pixel_to_mm_ratio = None # 像素-mm的转换单位
    width_px = None # 红肿块的像素宽度
    length_px = None # 红肿块的像素长度

    for path, img, im0s, vid_cap in dataset:
        img = torch.from_numpy(img).to(device)
        img = img.half() if half else img.float()  # uint8 to fp16/32
        img /= 255.0  # 0 - 255 to 0.0 - 1.0
        if img.ndimension() == 3:
            img = img.unsqueeze(0)

        # Warmup
        if device.type != 'cpu' and (old_img_b != img.shape[0] or old_img_h != img.shape[2] or old_img_w != img.shape[3]):
            old_img_b = img.shape[0]
            old_img_h = img.shape[2]
            old_img_w = img.shape[3]
            for i in range(3):
                model(img, augment=augment)[0]

        # Inference
        t1 = time_synchronized()
        with torch.no_grad():   # Calculating gradients would cause a GPU memory leak
            pred = model(img, augment=augment)[0]
        t2 = time_synchronized()

        # Apply NMS
        pred = non_max_suppression(pred, conf_thres, iou_thres, classes=classes, agnostic=agnostic_nms)
        t3 = time_synchronized()

        # Process detections
        for i, det in enumerate(pred):  # detections per image
            if webcam:  # batch_size >= 1
                p, s, im0, frame = path[i], '%g: ' % i, im0s[i].copy(), dataset.count
            else:
                p, s, im0, frame = path, '', im0s, getattr(dataset, 'frame', 0)

            p = Path(p)  # to Path
            save_path = str(save_dir / p.name)  # img.jpg
            txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}')  # img.txt
            gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwh
            if len(det):
                # Rescale boxes from img_size to im0 size
                det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()

                # Print results
                for c in det[:, -1].unique():
                    n = (det[:, -1] == c).sum()  # detections per class
                    s += f"{n} {names[int(c)]}{'s' * (n > 1)}, "  # add to string

                # Write results
                for *xyxy, conf, cls in reversed(det):
                    # 对检测结果进行后处理
                    # 如果是类别1（农夫山泉瓶盖），则计算瓶盖检测框的宽度用于计算比例
                    if int(cls) == 1:  
                        bottle_width = abs(xyxy[2] - xyxy[0])  # 计算瓶盖的宽度像素值
                        pixel_to_mm_ratio = bottle_diameter / bottle_width
                        break # 跳出循环

                for *xyxy, conf, cls in reversed(det):
                    # 如果是类别0（红肿块），计算宽度和高度并存储用于平均直径计算
                    if int(cls) == 0:
                        width_px = abs(xyxy[2] - xyxy[0])
                        length_px = abs(xyxy[3] - xyxy[1])
                        average_diameter = (width_px + length_px) / 2 * pixel_to_mm_ratio
                        red_bump_size = average_diameter

                        # 计算红肿块平均直径并给出诊断结果
                        if red_bump_size:
                            if red_bump_size < 5:
                                result = "阴性"
                            elif 5 <= red_bump_size < 10:
                                result = "一般阳性"
                            elif 10 <= red_bump_size < 15:
                                result = "中度阳性"
                            else:
                                result = "强阳性"
                        else:
                            result = None

                        result_info = {
                            "result": result if result else "None",
                            "width": (width_px * pixel_to_mm_ratio).item(),
                            "length": (length_px * pixel_to_mm_ratio).item(), 
                            "size": red_bump_size.item()
                        }
                        print(result_info)
                        
                    if save_txt:  # Write to file
                        xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()  # normalized xywh
                        line = (cls, *xywh, conf) if save_conf else (cls, *xywh)  # label format
                        with open(txt_path + '.txt', 'a') as f:
                            f.write(('%g ' * len(line)).rstrip() % line + '\n')

                    if save_img or view_img:  # Add bbox to image
                        label = f'{names[int(cls)]} {conf:.2f}'
                        # 将检测文本内容添加到结果集中通过接口返回
                        result_text.append(label)
                        plot_one_box(xyxy, im0, label=label, color=colors[int(cls)], line_thickness=3)

                

            # Print time (inference + NMS)
            print(f'{s}Done. ({(1E3 * (t2 - t1)):.1f}ms) Inference, ({(1E3 * (t3 - t2)):.1f}ms) NMS')

            # Stream results
            if view_img:
                cv2.imshow(str(p), im0)
                cv2.waitKey(1)  # 1 millisecond

            # Save results (image with detections) 
            # !在tmp的resuls路径下保存检测后的图片，如果想测试可以取消注释
            # if save_img:
            #     if dataset.mode == 'image':
            #         cv2.imwrite(save_path, im0)
            #         print(f" The image with the result is saved in: {save_path}")
            #     else:
            #         pass


            # 将图片保存到内存中最终通过接口返回
            _, im0_arr = cv2.imencode('.jpg', im0)
            im0_bytes = im0_arr.tobytes()
            result_image = base64.b64encode(im0_bytes).decode('utf-8')    

    # !在tmp的results/labels路径下保存检测后的类别和置信度信息文件，如果想测试可以取消注释
    # if save_txt or save_img:
    #     s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
    #     print(f"Results saved to {save_dir}{s}")

    # !删除tmp/inputs路径中保存的用户上传的图片，如果想测试可以取消注释
    Path(image_url).unlink()  # 使用 Path.unlink()

    return {
        "result_image": result_image,
        "result_text": result_text,
        "result_info": result_info,
        "time": f'Done. ({time.time() - t0:.3f}s)'
    }


# 定义 Pydantic 模型来解析请求体
class ImageRequest(BaseModel):
    image_url: str  # 图片的 URL

@app.post("/detect")
async def detect_endpoint(image_request: ImageRequest):
    image_url = image_request.image_url
    return detect(image_url)


@app.post("/detect/image")
async def detect_endpoint(image: UploadFile = File(...)):
    # 创建保存文件的目录
    input_dir = Path('tmp/input') # 保存结果的目录
    input_dir.mkdir(parents=True, exist_ok=True)  # 确保目录存在
    # 使用时间戳生成唯一文件名
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    image_url = input_dir / f"{timestamp}_{image.filename}"
    
    # 保存上传的文件到本地
    with open(image_url, "wb") as buffer:
        buffer.write(await image.read())

    print('User upload image saved as:',image_url)

    return detect(str(image_url))


@app.get("/")
async def test():
    return 'Welcome to PPD !'

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=5000)
